在今天的挑戰中,我們要加入一個重要的功能,就是掃描發票 QRCode 來幫助使用者加入家用品。這個功能會幫助使用者能夠更快速、方便的加入所購買的家用品。雖然今天的目標只集中在掃描和取得內容,但這將為接下來的工作打下基礎。
在新增頁面的右上角新增一個相機按鈕,讓使用者可以點擊開啟相機進行 QRCode 掃描。
struct AddItemView: View {
@ObservedObject var viewModel: AddItemViewModel
@State private var scanResult: String = "No QR code detected" // 儲存掃描結果
var body: some View {
VStack {
//...中間略
.navigationBarItems(trailing: NavigationLink(destination: QRScannerView(result: $scanResult)) {
Image(systemName: "camera")
.font(.title2)
})
}
}
}
這段程式碼新增了一個 NavigationLink
,它會打開我們稍後實作的 QRScannerView。
接著我們需要建立一個 QRScannerView,使用 UIViewControllerRepresentable 來將 UIViewController 包裝成 SwiftUI 可以使用的 View。
UIViewControllerRepresentable 是 SwiftUI 中的一個協定,用來將 UIKit 的 UIViewController 轉換成 SwiftUI 的 View,使我們可以在 SwiftUI 中使用現有的或自定義的 UIViewController。它允許我們在 SwiftUI 的架構中引入 UIKit 的功能,像是使用相機、地圖、或是其他 UIKit 的 Controller。
參考資料:
struct QRScannerView: UIViewControllerRepresentable {
@Binding var result: String // 掃描結果
func makeUIViewController(context: Context) -> QRScannerController {
let scannerController = QRScannerController()
scannerController.delegate = context.coordinator // 設置 delegate 處理掃描結果
return scannerController
}
func updateUIViewController(_ uiViewController: QRScannerController, context: Context) {
// 無需更新
}
func makeCoordinator() -> Coordinator {
Coordinator($result)
}
}
這裡使用 UIViewControllerRepresentable 來將 QRScannerController 引入到 SwiftUI 畫面中,並透過 Coordinator 來處理掃描結果。
Coordinator 負責接收來自相機掃描的結果,並將結果傳回 SwiftUI。
在 SwiftUI 中,Coordinator 的作用是充當橋樑,讓 UIKit 的控制器(例如相機)與 SwiftUI 進行溝通。由於 SwiftUI 和 UIKit 是兩個不同的框架,處理交互事件時,我們需要一個協調者來處理 SwiftUI 和 UIKit 之間的資料傳遞,這就是 Coordinator 的用途。
class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
@Binding var scanResult: String
init(_ scanResult: Binding<String>) {
self._scanResult = scanResult
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, metadataObject.type == .qr {
if let stringValue = metadataObject.stringValue {
scanResult = stringValue // 更新掃描結果
}
}
}
}
這段程式負責當掃描到 QRCode 後,將內容更新 scanResult。
在 iOS App 中,使用相機等個人資訊時,必須向使用者請求相對應的權限。在這次我們的目標中,掃描 QRCode 需要用到相機,因此我們必須在 Info.plist 中新增相機使用權限的說明。
<key>NSCameraUsageDescription</key>
<string>需要使用相機來掃描 QRCode</string>
NSCameraUsageDescription:這是 Apple 要求的關鍵,用來解釋為什麼應用需要使用相機。在 App 運行時,當我們第一次嘗試開啟相機時,iOS 會彈出一個對話框,顯示這個描述,讓使用者了解這一權限的用途。
QRScannerController 是一個 UIViewController,負責顯示相機並進行 QRCode 掃描。當掃描到 QRCode 後,它會將結果傳回 SwiftUI。
首先需要建立一個自定義的 UIViewController 來管理相機的掃描功能。
class QRScannerController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
var captureSession = AVCaptureSession()
var videoPreviewLayer: AVCaptureVideoPreviewLayer?
var qrCodeFrameView: UIView?
var onQRCodeScanned: ((String) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
checkCameraAuthorization()
}
}
參考資料:QRCode掃起來!
我們要先取得相機的使用權限,因此需要加入檢查和請求相機使用權限的邏輯。
func checkCameraAuthorization() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
// 已授權,開始配置相機
setupCamera()
case .notDetermined:
// 尚未決定,請求授權
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
if granted {
self.setupCamera()
} else {
self.showPermissionAlert() // 顯示權限不足提示
}
}
}
case .denied, .restricted:
// 已被拒絕或限制,顯示提示
showPermissionAlert()
@unknown default:
fatalError("Unexpected case for camera permission.")
}
}
已經取得相機權限後,可以開始設定相機的輸入和輸出,並將相機畫面顯示在螢幕上。
func setupCamera() {
captureSession = AVCaptureSession()
guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
print("無法取得相機裝置")
return
}
do {
let input = try AVCaptureDeviceInput(device: captureDevice)
captureSession.addInput(input)
let captureMetadataOutput = AVCaptureMetadataOutput()
captureSession.addOutput(captureMetadataOutput)
// 設置 delegate 以監聽 QRCode 掃描結果
captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
captureMetadataOutput.metadataObjectTypes = [.qr]
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer?.videoGravity = .resizeAspectFill
videoPreviewLayer?.frame = view.layer.bounds
view.layer.addSublayer(videoPreviewLayer!)
DispatchQueue.global(qos: .background).async {
self.captureSession.startRunning()
}
} catch {
print("Error occurred while setting up camera: \(error)")
}
}
當掃描到 QRCode 時,會觸發回調方法,我們可以在這裡處理掃描結果,並且提供使用者震動反饋。
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue else { return }
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
qrCodeFrameView?.frame = videoPreviewLayer?.transformedMetadataObject(for: metadataObject)?.bounds ?? .zero
onQRCodeScanned?(stringValue)
}
}
如果使用者拒絕相機權限,我們需要提醒他去設定中打開權限。
func showPermissionAlert() {
let alert = UIAlertController(title: "相機權限不足", message: "請到設置中開啟相機權限", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "確定", style: .default))
present(alert, animated: true)
}
有點長,這邊提供完整程式碼:
class QRScannerController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
var captureSession = AVCaptureSession()
var videoPreviewLayer: AVCaptureVideoPreviewLayer?
var qrCodeFrameView: UIView?
var onQRCodeScanned: ((String) -> Void)?
var delegate: AVCaptureMetadataOutputObjectsDelegate?
override func viewDidLoad() {
super.viewDidLoad()
checkCameraAuthorization()
}
// 掃描 QRCode 後觸發
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let stringValue = metadataObject.stringValue {
print("QR Code: \(stringValue)") // Debug 用
}
}
func checkCameraAuthorization() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
setupCamera() // 已授權,開始配置相機
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
DispatchQueue.main.async {
self.setupCamera()
}
}
}
default:
print("未授權使用相機")
}
}
func setupCamera() {
guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
print("無法取得相機裝置")
return
}
do {
let input = try AVCaptureDeviceInput(device: captureDevice)
captureSession.addInput(input)
let captureMetadataOutput = AVCaptureMetadataOutput()
captureSession.addOutput(captureMetadataOutput)
captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
captureMetadataOutput.metadataObjectTypes = [.qr]
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer?.videoGravity = .resizeAspectFill
videoPreviewLayer?.frame = view.layer.bounds
view.layer.addSublayer(videoPreviewLayer!)
captureSession.startRunning()
} catch {
print("相機初始化失敗: \(error)")
}
}
}
QRScannerController 是一個相機的 Controller,使用 AVCaptureSession 來配置相機,並處理 QRCode 掃描結果。
今天練習到如何在 SwiftUI 使用 UIKit,沒有想到這兩樣東西要連結起來這麼麻煩的事情!這次實作 QRCode 掃描功能,包括在 AddItemView 中新增一個相機按鈕,開啟 QRCode 掃描畫面,並取得 QRCode 中的資料。明天我們再接續解析資料,製作 QRCode 掃描加入家用品功能吧!